"""
Fupper - Flickr upload tool for Series 60
Copyright (C) 2006 Teemu Harju

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

teemu.harju@gmail.com

$Author: teemu.harju $
$Rev: 26 $
$LastChangedDate: 2007-07-21 05:35:47 +0000 (Sat, 21 Jul 2007) $
"""

import os
import sys
import math
import time
import sysinfo
import thread
import re

import appuifw
import e32
from topwindow import *

from pys60flickrapi import FlickrAPI
from pys60flickrapi import FlickrException
from graphics import Image
from key_codes import EKeyLeftArrow, EKeyRightArrow, EKeySelect, EKeyDownArrow, EKeyUpArrow

from __main__ import animation, frame_pos, text_pos, APPNAME, APPABOUT, APPPATH

APPLICATION = u'Fupper'

ENTRIES = [(u'Phone Memory', u'Images from phone memory'),
	   (u'Memory Card', u'Images from memory card')]
DIRS = None
if e32.s60_version_info >= (3,0):
    DIRS = [u'c:\\Data\\Images', u'e:\\Images']
else:
    DIRS = [u'c:\\Nokia\Images', u'e:\\Images']
LOGO_IMAGES = []

BACK_TO_MENU = -1
SHOW_PREVIEW = -2

AUTH_TEXT = u"Authentication request:\nGo to www.flickr.com/auth-21694."

THUMBNAIL_SIZE = (80,60)
THUMBNAIL_POS = (appuifw.app.body.size[0] - (THUMBNAIL_SIZE[0] + 40), 10)

#-----------------------------------------------------------------------
def __thumbnail_loaded_callback(index, thumb):
    print "Thumbnail loaded callback!"
    THUMBNAILS[index] = thumb

CALLGATE = e32.ao_callgate(__thumbnail_loaded_callback)
TOPWIN = TopWindow()
TOPWIN.size = THUMBNAIL_SIZE
TOPWIN.shadow = 3
TOPWIN.position = THUMBNAIL_POS
THUMBNAILS = {}

class Fupper:
    """ Base class for the Fupper application."""

    #-----------------------------------------------------------------------
    def __init__(self):
        """ Constructor for Fupper class.
        
        It stores the Flickr API key and shared secret and manages the different
        states of the application as well as the menu structure.
        """
	self.script_lock = e32.Ao_lock()
	self.thumb_lock = None # created in the thumbnail thread
	self.thumb_thread_id = None
	self.lb = appuifw.Listbox(ENTRIES, self.__lbox_observe)
	self.current_dir = None
	self.files = []
	self.flickrAPIKey = "bb627fee1d9f3ee5ec8f278bf68e20a6"
	self.flickrSecret = "4237c2e13905d4e8"
        self.flickrAPI = FlickrAPI(self.flickrAPIKey, self.flickrSecret)
        self.flickrAuthToken = self.flickrAPI.checkToken(perms="write")

        # authenticate Fupper if no authToken was found from memory
        self.__authenticate()
                
        self.photosets = None # self.__get_photosets()
        self.rsp = None
        self.uploading_photo = 0
        self.thumbnails_loaded = 0
        self.thumbnail_lock = thread.allocate_lock()
	self.canvas = None
        self.img = None
        #self.thumbnails = {}
        #self.topwin = TopWindow()
        #self.topwin.size = THUMBNAIL_SIZE
        #self.topwin.shadow = 3
        #self.topwin.position = THUMBNAIL_POS
        self.preview_running = 0
        self.preview_loaded = 0
        self.top_menu = [(u'Open',
                          lambda:self.__lbox_observe(self.lb.current())),
                         (u'About', lambda:appuifw.note(APPABOUT)),
                         (u'Exit', self.__exit_key_handler)
                        ]
        self.dir_menu = [(u'Show preview',
                          lambda:self.__show_preview(self.lb.current())),
                         (u'Upload to Flickr',
                          lambda:self.__upload_to_flickr(self.lb.current())),
                         (u'Back',
                          lambda:self.__lbox_observe(BACK_TO_MENU)),
                         (u'Exit', self.__exit_key_handler)
                         ]
        self.preview_menu = [(u'Upload to Flickr',
                              lambda:self.__upload_to_flickr(self.lb.current())),
                             (u'Back',
                              lambda:self.__preview_event_callback({'keycode': EKeyLeftArrow}))
                            ]
        appuifw.app.menu = self.top_menu
        appuifw.app.exit_key_handler = self.__exit_key_handler

        #self.thumb_callgate = e32.ao_callgate(self.__thumbnail_loaded_callback)

    #-----------------------------------------------------------------------
    def run(self):
        """ Runs the Fupper application."""
	self.lb.bind(EKeyLeftArrow, lambda: self.__lbox_observe(BACK_TO_MENU))
	self.lb.bind(EKeyRightArrow, lambda: self.__lbox_observe(SHOW_PREVIEW))
	self.lb.bind(EKeyUpArrow, lambda: self.__thumbnail_callback(EKeyUpArrow))
	self.lb.bind(EKeyDownArrow, lambda: self.__thumbnail_callback(EKeyDownArrow))
	self.__refresh()
	self.script_lock.wait()

    #-----------------------------------------------------------------------
    def __refresh(self, menu=None):
        """ Refreshes the UI of the application.

        menu -- list of tuples containing the menu
        """
	appuifw.app.title = APPNAME
        if menu != None:
            appuifw.app.menu = menu
        appuifw.app.body = self.lb

    #-----------------------------------------------------------------------
    def __exit_key_handler(self):
        """ Exits the application."""
        if appuifw.query(u'Are you sure you want to exit?', 'query'):
            self.preview_running = 0
            self.script_lock.signal()
            appuifw.app.exit_key_handler = None
            appuifw.app.set_exit()
        else:
            pass

    #-----------------------------------------------------------------------
    def __lbox_observe(self, ind=None):
        """Monitors the listbox that is used to browse files etc in the
        user interface.

        ind -- index number used for different commands to the listbox
        """

	if ind <> None:
	    index = ind
	else:
	    index = self.lb.current()
	    
        print "Index: %s" % str(index)
        
        if index >= 0:
	    if self.current_dir == None:
		self.current_dir = index
		print "self.current_dir = %s" % str(self.current_dir)
		print "Current directory: %s" % DIRS[self.current_dir]
		entries = []
		for file in os.listdir(DIRS[index]):
		    path = os.path.join(DIRS[index], file)
                    # only .jpg and .png files are supported
		    if os.path.isfile(path) and (path.endswith('.jpg') or
                                                 path.endswith('.png')):
			self.files.append(file)
			entries.append((unicode(file),
                                        self.__get_file_modified_str(path) +
                                        self.__get_file_size_str(path)))
		if len(self.files) > 0:
		    self.lb.set_list(entries)
		    # start loading thumbnails
		    print "Should start loading thumbnails now..."
		    if not self.thumbnails_loaded and self.current_dir <> None:
                        print "Starting the thumbnail thread..."
                        self.thumb_thread_id = thread.start_new_thread(self.__load_thumbnails_thread, ())
                    self.__refresh(self.dir_menu)
                else:
		    appuifw.note(u'No images in ' + ENTRIES[index][0]);
		    self.current_dir = None
	    else:
                self.__upload_to_flickr(self.lb.current())
		
	elif index == BACK_TO_MENU:
	    self.current_dir = None
	    #self.thumb_lock.signal()
	    thread.ao_waittid(self.thumb_thread_id) # wait for the thread to exit
	    TOPWIN.hide()
	    self.thumbnails_loaded = 0
	    THUMBNAILS = {}
	    TOPWIN.images = []
	    print "Thumbnails cleared..."
	    self.files = []
	    self.lb.set_list(ENTRIES)
            self.__refresh(self.top_menu)

	elif index == SHOW_PREVIEW:
	    if self.current_dir == None: # we are in the main menu
		pass # do nothing
	    else:
		self.__show_preview(self.lb.current())

    #-----------------------------------------------------------------------
    def __get_file_size_str(self, path):
        """Returns the size of the file in kilobytes as a string formatted
        as XXX kB.

        path -- path to the file
        """
        return unicode(self.__get_kbytes(os.stat(path).st_size)) + u' kB'

    #-----------------------------------------------------------------------
    def __get_kbytes(self, bytes):
        """Returns filesize in kilobytes.

        bytes -- number of bytes to be converted to kilobytes
        """
	return bytes/1024

    #-----------------------------------------------------------------------
    def __get_file_modified_str(self, path):
        """Returns a string of the date when the file was last modified.

        path -- path to the file
        """
	return time.strftime("%d.%m.%Y %H:%M ",
			     time.localtime(os.stat(path).st_mtime));

    #-----------------------------------------------------------------------
    def __show_preview(self, index):
        """ Shows a preview of a photo.

        index -- index of the file in listbox self.lb
        """
        self.preview_running = 1
        appuifw.app.menu = self.preview_menu
        if self.canvas == None:
            self.canvas = appuifw.Canvas(redraw_callback=self.__handle_redraw,
                                         event_callback=self.__preview_event_callback)
        self.canvas.clear()
        self.img = None
	path = os.path.join(DIRS[self.current_dir], self.files[index])
        imgsize = Image.inspect(path)['size']
	self.img = Image.new(imgsize)
        self.preview_loaded = 0
        self.img.load(path, callback=self.__preview_loaded_callback)
	appuifw.app.body = self.canvas
        while self.preview_running:
            frame = 0
            while not self.preview_loaded:
                self.canvas.blit(animation[frame%17], frame_pos)
                frame = frame + 1
                e32.ao_sleep(0.2)
            self.__handle_redraw(())
            e32.ao_yield()
        self.__refresh(self.dir_menu)

    #-----------------------------------------------------------------------
    def __preview_loaded_callback(self, err):
        """Callback function for image preview.

        This function is called when the image to be previewed has been
        loaded completely.
        """
        self.preview_loaded = 1

    #-----------------------------------------------------------------------
    def __handle_redraw(self, rect):
        """Redraws the canvas where the image preview is shown."""
        self.canvas.blit(self.img, target=((0,0), self.canvas.size), scale=1)

    #-----------------------------------------------------------------------
    def __preview_event_callback(self, arg):
        """Callback function that handles the key events while the preview
        of the photo is running.
        """
        if arg['keycode'] == EKeyLeftArrow:
            self.preview_running = 0
        elif arg['keycode'] == EKeySelect:
            self.__upload_to_flickr(self.lb.current())
        else:
            pass
    
    #-----------------------------------------------------------------------
    def __load_thumbnails_thread(self):
        """Loads thumbnail images form given entries.

        This function is run in background while browsing the list of image
        files.
        """
        global thumb
        print "Thumb thread started.."
        #if not self.thumb_lock:
        #    self.thumb_lock = e32.Ao_lock()
        index = 0
        for filename in self.files:
            print "Loading file %s" % filename
            temp = Image.open(unicode(os.path.join(DIRS[self.current_dir], filename)))
            # scaling should be faster than resizing so we use that here
            thumb = Image.new(THUMBNAIL_SIZE)
            thumb.blit(temp, target=((0,0), THUMBNAIL_SIZE), scale=1)
            print "Calling CALLGATE..."
            CALLGATE(index, thumb)
            print "CALLGATE returned..."
            index = index + 1
        self.thumbnails_loaded = 1
        print "Thumbnails loaded!"
        #self.thumb_lock.wait()
        #print "Thumb lock released!"

    #-----------------------------------------------------------------------
    def __thumbnail_callback(self, key):
        """A callback function that is called whenever the selected element
        in the directory listing changes. This function then sets the
        thumbnail to the topwindow if it has been loaded by __load_thumbnails().
        """
        if self.current_dir <> None:
            index = self.lb.current()
            last_index = len(self.files) - 1
            if key == EKeyUpArrow and index == 0: # top of the list moving up
                index = last_index
            elif key == EKeyDownArrow and index == last_index: # at the end and going down
                index = 0
            elif key == EKeyUpArrow:
                index = index - 1
            elif key == EKeyDownArrow:
                index = index + 1
            else:
                pass # we should not end up here at all
            if index in THUMBNAILS:
                TOPWIN.images = [(THUMBNAILS[index], (0,0))]
                if not TOPWIN.visible:
                    TOPWIN.show()
            else:
                if TOPWIN.visible:
                    TOPWIN.hide()
                TOPWIN.images = [] # clear thumbnails

    #-----------------------------------------------------------------------
    def __verify_token(self, token):
        """Verifys the given minitoken.

        First of all this function checks if the given minitoken is of
        correct format and then gets the full authentication token from
        Flickr. The token is stored in the memory so authentication is
        needed only once.

        token -- string of the authentication token
        """
        if re.match("^[0-9]{3}-[0-9]{3}-[0-9]{3}$", token):
            try:
                self.flickrAuthToken = self.flickrAPI.getFullToken(token)
                return 1
            except FlickrException, e:
                appuifw.note(unicode(e.msg), 'error')
                return 0
            except IOError, e:
                appuifw.note(u'Connection failed!', 'error')
        else:
            return 0

    #-----------------------------------------------------------------------
    def __authenticate(self):
        """Authenticates the application to Flickr.

        Basically only asks user to insert the minitoken found from Flickr
        site and calls __verify_token to check and store the token for
        later use. If user cancels, the application exits.
        """
        if self.flickrAuthToken:
            pass # do nothing, app is authenticated
        else:
            proceed = appuifw.query(AUTH_TEXT, 'query')
            if proceed:
                miniToken = appuifw.query(u'Give the mini-token:', 'text')
                while not self.__verify_token(miniToken):
                    miniToken = appuifw.query(u'Invalid token! Try again:', 'text')
                    if miniToken == 0: # user has pressed cancel
                        return
                appuifw.note(u'Token accepted!')
            else:
                sys.exit() # user did not want to authenticate the app
        appuifw.note(u'Logged in to Flickr as %s.' % self.flickrAPI.fullname)

    #-----------------------------------------------------------------------
    def __upload_to_flickr(self, index):
        """Uploads photo to Flickr.

        index -- index of the photo in listbox self.lb
        """
        if appuifw.query(u'Do you want to upload this image to Flickr?',
                         'query'):
            title = appuifw.query(u'Give title for the photo:', 'text')
            if title == None:
                title = self.files[index] # use filename if title not given
            desc = appuifw.query(u'Give description for the photo:', 'text')
            if desc == None:
                desc = ""
            tags = appuifw.query(u'Give tags for the photo:', 'text')
            if tags == None:
                tags = "fupper"
            else :
                tags = tags + " fupper"
            tags = tags.strip()
            is_public = is_friend = is_family = "0"
            if appuifw.query(u'Do you want to make this photo private?', 'query'):
                is_public = "0" # photo will be private
                if appuifw.query(u'Allow friends or family see this photo too?', 'query'):
                    acl = appuifw.multi_selection_list([u'Visible to friends', u'Visible to family'])
                    if len(acl) == 0:
                        appuifw.note(u'Only you can view the photo.')
                    elif len(acl) == 2: # both choises have been selected
                        is_friend = "1"
                        is_family = "1"
                    elif acl[0] == 0:
                        is_friend = "1"
                    else:
                        is_family = "1"
                else:
                    appuifw.note(u'Only you can view the photo.')
            else:
                appuifw.note(u'Photo will be public.')
                is_public = "1"
                    
            photo = os.path.join(DIRS[self.current_dir], self.files[index])

            appuifw.note(u'Uploading %s...' % self.files[index])
            
            self.uploading_photo = 1
            thread.start_new_thread(self.__upload_thread,
                                    (photo, title, desc, tags, is_public, is_friend, is_family))
            frame = 0
            canvas = appuifw.Canvas()
            canvas.clear()
            appuifw.app.body = canvas
            while self.uploading_photo:
                canvas.blit(animation[frame%17], frame_pos)
                canvas.text(text_pos, u"Uploading...")
                frame = frame + 1
                e32.ao_sleep(0.2)
            
            if self.rsp == None:
                print("can't find file\n")
            else:
                self.flickrAPI.testFailure(self.rsp)
                appuifw.note(u'Photo uploaded!')
                self.__add_to_photoset(self.rsp.photoid[0].elementText)
            self.__refresh()
        else:
            pass

    #-----------------------------------------------------------------------
    def __upload_thread(self, filename, title, desc, tags, is_public, is_friend, is_family):
        """Thread used to upload the photo in the background

        filename -- name of the uploaded file
        title -- title of the photo
        desc -- description of the photo
        tags -- Flickr tags
        is_public -- whether the photo is public or not
        """
        self.rsp = self.flickrAPI.upload(filename=filename,
                                    api_key=self.flickrAPIKey,
                                    auth_token=self.flickrAuthToken, 
                                    title=title, description=desc,
                                    tags=tags,
                                    is_public=is_public,
                                    is_friend=is_friend,
                                    is_family=is_family)
        self.uploading_photo = 0

    #-----------------------------------------------------------------------
    def __get_photosets(self):
        """Returns list of user's photosets

        returns -- list of tuples of photoset id and title
        """
        rsp = self.flickrAPI.photosets_getList(api_key=self.flickrAPIKey,
                                               auth_token=self.flickrAuthToken)
        self.flickrAPI.testFailure(rsp)
        photoset_list = []
        for photoset in rsp.photosets[0].photoset:
            photoset_list.append((photoset['id'], unicode(photoset.title[0].elementText)))
        return photoset_list

    #-----------------------------------------------------------------------
    def __add_to_photoset(self, photoid):
        """Asks whether the user wants to add the photo to some photoset and
        then adds it.

        photoid -- flickr photoid of the photo to be added
        """
        if appuifw.query(u'Do you want to add this photo to some photoset?', 'query'):
            photosets = []
            if self.photosets == None:
                appuifw.note(u'Getting photosets...')
                self.photosets = self.__get_photosets()
            for photoset in self.photosets:
                photosets.append(photoset[1])
            
            index = appuifw.popup_menu(photosets)
            rsp = self.flickrAPI.photosets_addPhoto(api_key=self.flickrAPIKey,
                                                    auth_token=self.flickrAuthToken,
                                                    photoset_id=self.photosets[index][0],
                                                    photo_id=photoid)
            self.flickrAPI.testFailure(rsp)
            appuifw.note(u'Photo added to photoset!')
        else:
            pass

def run():
    f = Fupper()
    if e32.s60_version_info >= (3,0):
        f.run()
    else:
        e32.ao_sleep(0, f.run)

if __name__ == '__main__':
    run()
